﻿using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Runtime.Caching;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.DomainServices.Hosting;
using System.ServiceModel.DomainServices.Server;
using log4net;

// http://mvvm.granwehr.net/MVVMtutorialAdvanced-Web-DomainServiceBook.svc
// Test Webservice http://localhost:46620/MVVMtutorialAdvanced-Web-DomainServiceBook.svc

namespace MVVMtutorialAdvanced.Web
{
	[EnableClientAccess]
	[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall, ConcurrencyMode = ConcurrencyMode.Single)]
	[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] // Must for the DomainServiceFactory, because aspNetCompatibilityEnabled="true"
	public class DomainServiceBook : DomainService
	{
		private static readonly ILog _log = LogManager.GetLogger(typeof(DomainServiceBook));

		private object thisLock = new object();

		private static List<CountryDTO> _countries;
		private static List<AuthorDTO> _authors;
		private static List<BookDTO> _books;
		private static List<BookAuthorDTO> _bookAuthors;
		private const bool useSaveAll = false;

		public DomainServiceBook()
		{
			try
			{
				// All Data is null (after restart of service)
				if (_authors == null)
				{
					_log.Debug("DomainServiceBook.Constructor. _authors is null");

					var basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data");

					// Get Data from files, if available
					if (File.Exists(Path.Combine(basePath, "_countries.xml")))
					{
						lock (thisLock)
						{
							var serializer = new NetDataContractSerializer();

							var fs = File.OpenRead(Path.Combine(basePath, "_countries.xml"));
							fs.Position = 0;
							_countries = (List<CountryDTO>) serializer.Deserialize(fs);
							fs.Close();

							fs = File.OpenRead(Path.Combine(basePath, "_authors.xml"));
							fs.Position = 0;
							_authors = (List<AuthorDTO>) serializer.Deserialize(fs);
							fs.Close();

							fs = File.OpenRead(Path.Combine(basePath, "_books.xml"));
							fs.Position = 0;
							_books = (List<BookDTO>) serializer.Deserialize(fs);
							fs.Close();

							fs = File.OpenRead(Path.Combine(basePath, "_bookAuthors.xml"));
							fs.Position = 0;
							_bookAuthors = (List<BookAuthorDTO>) serializer.Deserialize(fs);
							fs.Close();

							return;
						}
					}

					// No data in files available -> create new dummy data

					_countries = new List<CountryDTO>();

					//Due to an error in the April 2010 ComboBox, NULL cannot be used  (int?)
					//https://connect.microsoft.com/VisualStudio/feedback/details/597689/silverlight-combobox-binding-drops-after-null-binding

					_countries.Add(new CountryDTO { Id = 0, Alias = "-" });
					_countries.Add(new CountryDTO { Id = 1, Alias = "Schweiz" });
					_countries.Add(new CountryDTO { Id = 2, Alias = "Deutschland" });
					_countries.Add(new CountryDTO { Id = 3, Alias = "Italien" });
					_countries.Add(new CountryDTO { Id = 4, Alias = "Frankreich" });
					_countries.Add(new CountryDTO { Id = 5, Alias = "USA" });


					_authors = new List<AuthorDTO>();

					var author1 = new AuthorDTO { Id = 1, FirstName = "Michael C", LastName = "Feathers" };
					_authors.Add(author1);
					var author2 = new AuthorDTO { Id = 2, FirstName = "Martin", LastName = "Fowler" };
					_authors.Add(author2);
					var author3 = new AuthorDTO { Id = 3, FirstName = "Erich", LastName = "Gamma" };
					_authors.Add(author3);
					var author4 = new AuthorDTO { Id = 4, FirstName = "Richard", LastName = "Helm" };
					_authors.Add(author4);
					var author5 = new AuthorDTO { Id = 5, FirstName = "Ralph", LastName = "Johnson" };
					_authors.Add(author5);
					var author6 = new AuthorDTO { Id = 6, FirstName = "John M", LastName = "Vlissides" };
					_authors.Add(author6);


					_bookAuthors = new List<BookAuthorDTO>();
					var bookAuthor1 = new BookAuthorDTO { Id = 1, Name = "Michael C Feathers", AuthorId = 1, BookId = 1 };
					_bookAuthors.Add(bookAuthor1);
					var bookAuthor2 = new BookAuthorDTO { Id = 2, Name = "Martin Fowler", AuthorId = 2, BookId = 2 };
					_bookAuthors.Add(bookAuthor2);
					var bookAuthor3 = new BookAuthorDTO { Id = 3, Name = "Erich Gamma", AuthorId = 3, BookId = 3 };
					_bookAuthors.Add(bookAuthor3);
					var bookAuthor4 = new BookAuthorDTO { Id = 4, Name = "Richard Helm", AuthorId = 4, BookId = 3 };
					_bookAuthors.Add(bookAuthor4);
					var bookAuthor5 = new BookAuthorDTO { Id = 5, Name = "Ralph Johnson", AuthorId = 5, BookId = 3 };
					_bookAuthors.Add(bookAuthor5);
					var bookAuthor6 = new BookAuthorDTO { Id = 6, Name = "John M Vlissides", AuthorId = 6, BookId = 3 };
					_bookAuthors.Add(bookAuthor6);


					var fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"bin\cover1.jpg");
					var cover1 = Image.FromFile(fileName);

					_books = new List<BookDTO>();

					var book1 = new BookDTO { Id = 1, Title = "Working Effectively with Legacy Code", Price = 45.2M, IsOutOfStock = false, Release = new DateTime(2004, 10, 2), CountryId = 5, Cover = ImageToByteArray(cover1) };
					book1.Authors.Add(bookAuthor1);
					_books.Add(book1);

					fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"bin\cover2.jpg");
					var cover2 = Image.FromFile(fileName);

					var book2 = new BookDTO { Id = 2, Title = "Patterns of Enterprise Application Architecture", Price = 43.25M, IsOutOfStock = true, Cover = ImageToByteArray(cover2) };
					book2.Authors.Add(bookAuthor2);
					_books.Add(book2);

					fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"bin\cover3.jpg");
					var cover3 = Image.FromFile(fileName);

					var book3 = new BookDTO { Id = 3, Title = "Design Patterns: Elements of Reusable Object-Oriented Software", Price = 37.75M, IsOutOfStock = true, CountryId = 5, Cover = ImageToByteArray(cover3) };
					book3.Authors.Add(bookAuthor3);
					book3.Authors.Add(bookAuthor4);
					book3.Authors.Add(bookAuthor5);
					book3.Authors.Add(bookAuthor6);
					_books.Add(book3);

					var policy = new CacheItemPolicy();
					policy.SlidingExpiration = new TimeSpan(0, 5, 0);


					// Save data to file
					SaveAll(true);
				}
				else
					_log.Debug("DomainServiceBook.Constructor. _authors is not null");

			}
			catch (Exception ex)
			{
				_log.Fatal("Error in DomainServiceBook.Constructor.", ex);
			}
		}

		// Can be used to persist all Data in a very brute manner
		private static void SaveAll(bool useSaveAll)
		{
			if (!useSaveAll)
				return;
			if (_authors == null)
				return;

			var serializer = new NetDataContractSerializer();

			_log.DebugFormat("SaveAll. BaseDirectory='{0}'", AppDomain.CurrentDomain.BaseDirectory);

			var basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "App_Data");

			var filestream = new FileStream(Path.Combine(basePath, "_countries.xml"), FileMode.Create, FileAccess.Write, FileShare.None);
			serializer.Serialize(filestream, _countries);
			filestream.Close();

			filestream = new FileStream(Path.Combine(basePath, "_authors.xml"), FileMode.Create, FileAccess.Write, FileShare.None);
			serializer.Serialize(filestream, _authors);
			filestream.Close();

			filestream = new FileStream(Path.Combine(basePath, "_books.xml"), FileMode.Create, FileAccess.Write, FileShare.None);
			serializer.Serialize(filestream, _books);
			filestream.Close();

			filestream = new FileStream(Path.Combine(basePath, "_bookAuthors.xml"), FileMode.Create, FileAccess.Write, FileShare.None);
			serializer.Serialize(filestream, _bookAuthors);
			filestream.Close();

			_log.DebugFormat("SaveAll. filestream.Name='{0}'", filestream.Name);
		}

		private static byte[] ImageToByteArray(Image imageIn)
		{
			var ms = new MemoryStream();
			imageIn.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
			return ms.ToArray();
		}

		// Overridden to support paging over POCO entities.
		protected override int Count<T>(IQueryable<T> query)
		{
			return query.Count();
		}


		#region country

		public ICollection<CountryDTO> GetCountries()
		{
			try
			{
				return _countries;
			}
			catch (Exception ex)
			{
				_log.Fatal("Error in GetCountries.", ex);
				return null;
			}
		}

		#endregion country


		#region author

		public IQueryable<AuthorDTO> GetAuthors()
		{
			try
			{
				_log.Debug("GetAuthors");

				System.Threading.Thread.Sleep(5000); //Show IsBusy

				var q =
					from aut in _authors
					select aut;

				return q.AsQueryable();
			}
			catch (Exception ex)
			{
				_log.Fatal("Error in GetAuthors.", ex);
				return null;
			}
		}

		public void InsertAuthor(AuthorDTO author)
		{
			try
			{
				_log.DebugFormat("InsertAuthor. author.Id='{0}'", author.Id);

				if (_authors.Contains(author))
					return;

				lock (thisLock)
				{
					if (_authors.Count == 0)
						author.Id = 1;
					else
						author.Id = _authors.Max(a => a.Id) + 1;

					_authors.Add(author);
					SaveAll(useSaveAll);
				}
			}
			catch (Exception ex)
			{
				if (author == null)
					_log.Fatal("Error in InsertAuthor. author is null", ex);
				else
					_log.Fatal(string.Format("Error in InsertAuthor. author.Id='{0}'", author.Id), ex);
			}
		}

		private static AuthorDTO GetAuthorById(int id)
		{
			try
			{
				_log.DebugFormat("GetAuthorById. id='{0}'", id);

				var q =
					from aut in _authors
					where aut.Id == id
					select aut;

				return q.FirstOrDefault();
			}
			catch (Exception ex)
			{
				_log.Fatal(string.Format("Error in GetAuthorById. id='{0}'", id), ex);
				return null;
			}
		}

		public void DeleteAuthor(AuthorDTO author)
		{
			try
			{
				_log.DebugFormat("DeleteAuthor. author.Id='{0}'", author.Id);

				lock (thisLock)
				{
					var authorToDelete = GetAuthorById(author.Id);
					_authors.Remove(authorToDelete);

					_bookAuthors.RemoveAll(ba => ba.AuthorId == author.Id);
					foreach (var book in _books)
					{
						var bookAuthorToDelete = book.Authors.Where(a => a.AuthorId == author.Id).FirstOrDefault();
						book.Authors.Remove(bookAuthorToDelete);
					}

					SaveAll(useSaveAll);
				}
			}
			catch (Exception ex)
			{
				if (author == null)
					_log.Fatal("Error in DeleteAuthor. author is null", ex);
				else
					_log.Fatal(string.Format("Error in DeleteAuthor. author.Id='{0}'", author.Id), ex);
			}
		}

		public void UpdateAuthor(AuthorDTO author)
		{
			try
			{
				_log.DebugFormat("UpdateAuthor. author.Id='{0}'", author.Id);

				lock (thisLock)
				{
					var authorToUpdate = GetAuthorById(author.Id);
					authorToUpdate.FirstName = author.FirstName;
					authorToUpdate.LastName = author.LastName;

					foreach (var bookAuthor in _bookAuthors.Where(ba => ba.AuthorId == author.Id))
						bookAuthor.Name = author.FirstName + " " + author.LastName;

					SaveAll(useSaveAll);
				}
			}
			catch (Exception ex)
			{
				if (author == null)
					_log.Fatal("Error in UpdateAuthor. author is null", ex);
				else
					_log.Fatal(string.Format("Error in UpdateAuthor. author.Id='{0}'", author.Id), ex);
			}
		}

		#endregion author


		#region book

		[Query(IsComposable = true, HasSideEffects = true)]
		public IQueryable<BookDTO> GetBooks()  // ?????????????????????????????????????? IQueryable ????????????????????????????????????
		{
			try
			{
				_log.Debug("GetBooks");

				System.Threading.Thread.Sleep(500); //Show IsBusy

				foreach (var book in _books)
					RefreshBookAuthors(book);

				return _books.AsQueryable();

				//var q =
				//  from book in _books
				//  select book;

				//return q.ToArray();
			}
			catch (Exception ex)
			{
				_log.Fatal("Error in GetBooks.", ex);
				return null;
			}
		}

		private static void RefreshBookAuthors(BookDTO book)
		{
			if (book == null)
				return;

			foreach (var author in book.Authors)
			{
				if (author == null)
					continue;
				var authorId = author.AuthorId;
					
				var x = _authors.Find(y => y.Id == authorId);
				if (x == null)
					author.Name = "Error. Could not find author.";
				else
					author.Name = x.FirstName + " " + x.LastName;
			}
		}

		public void InsertBook(BookDTO book)
		{
			try
			{
				_log.DebugFormat("InsertBook. book.Id='{0}'", book.Id);

				if (_books.Contains(book))
					return;

				lock (thisLock)
				{
					if (_books.Count == 0)
						book.Id = 1;
					else
						book.Id = _books.Max(a => a.Id) + 1;

					_books.Add(book);

					foreach (var bookAuthor in book.Authors)
					{
						if (_bookAuthors.Count == 0)
							bookAuthor.Id = 1;
						else
							bookAuthor.Id = _bookAuthors.Max(ba => ba.Id) + 1;
						bookAuthor.BookId = book.Id;

						_bookAuthors.Add(bookAuthor);
					}

					SaveAll(useSaveAll);
				}
			}
			catch (Exception ex)
			{
				if (book == null)
					_log.Fatal("Error in InsertBook. book is null", ex);
				else
					_log.Fatal(string.Format("Error in InsertBook. book.Id='{0}'", book.Id), ex);
			}
		}

		private static BookDTO GetBookById(int id)
		{
			try
			{
				_log.DebugFormat("GetBookById. id='{0}'", id);

				var q =
					from b in _books
					where b.Id == id
					select b;

				var book = q.FirstOrDefault();
				RefreshBookAuthors(book);

				return book;
			}
			catch (Exception ex)
			{
				_log.Fatal(string.Format("Error in GetBookById. id='{0}'", id), ex);
				return null;
			}
		}

		public void DeleteBook(BookDTO book)
		{
			try
			{
				_log.DebugFormat("DeleteBook. book.Id='{0}'", book.Id);

				lock (thisLock)
				{
					_bookAuthors.RemoveAll(x => x.BookId == book.Id);

					var bookToDelete = GetBookById(book.Id);
					_books.Remove(bookToDelete);

					SaveAll(useSaveAll);
				}
			}
			catch (Exception ex)
			{
				if (book == null)
					_log.Fatal("Error in DeleteBook. book is null", ex);
				else
					_log.Fatal(string.Format("Error in DeleteBook. book.Id='{0}'", book.Id), ex);
			}
		}

		public void UpdateBook(BookDTO book)
		{
			try
			{
				_log.DebugFormat("UpdateBook. book.Id='{0}'", book.Id);

				lock (thisLock)
				{
					var bookToUpdate = GetBookById(book.Id);
					bookToUpdate.Title = book.Title;
					bookToUpdate.Price = book.Price;
					bookToUpdate.IsOutOfStock = book.IsOutOfStock;
					bookToUpdate.Release = book.Release;
					bookToUpdate.CountryId = book.CountryId;
					bookToUpdate.Cover = book.Cover;

					foreach (var changeSetEntry in ChangeSet.ChangeSetEntries)
					{
						if (changeSetEntry.Entity.GetType() == typeof (BookAuthorDTO))
						{
							var bookAuthor = changeSetEntry.Entity as BookAuthorDTO;
							if (bookAuthor == null)
								continue;

							if (changeSetEntry.Operation == DomainOperation.Delete)
								_bookAuthors.RemoveAll(a => a.Id == bookAuthor.Id);
							else if (changeSetEntry.Operation == DomainOperation.Insert)
							{
								if (_bookAuthors.Count == 0)
									bookAuthor.Id = 1;
								else
									bookAuthor.Id = _bookAuthors.Max(ba => ba.Id) + 1;

								_bookAuthors.Add(bookAuthor);
							}
						}
					}

					bookToUpdate.Authors = book.Authors;

					SaveAll(useSaveAll);
				}
			}
			catch (Exception ex)
			{
				if (book == null)
					_log.Fatal("Error in UpdateBook. book is null", ex);
				else
					_log.Fatal(string.Format("Error in UpdateBook. book.Id='{0}'", book.Id), ex);
			}
		}

		#endregion book
	}
}


